/**
 * --------------------------------------------------------------------------
 * Bootstrap (v4.3.1): carousel.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

import $ from 'jquery'
import Util from './util'

/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */

const NAME = 'carousel'
const VERSION = '4.3.1'
const DATA_KEY = 'bs.carousel'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const JQUERY_NO_CONFLICT = $.fn[NAME]
const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key
const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
const SWIPE_THRESHOLD = 40

const Default = {
    interval: 5000,
    keyboard: true,
    slide: false,
    pause: 'hover',
    wrap: true,
    touch: true
}

const DefaultType = {
    interval: '(number|boolean)',
    keyboard: 'boolean',
    slide: '(boolean|string)',
    pause: '(string|boolean)',
    wrap: 'boolean',
    touch: 'boolean'
}

const Direction = {
    NEXT: 'next',
    PREV: 'prev',
    LEFT: 'left',
    RIGHT: 'right'
}

const Event = {
    SLIDE: `slide${EVENT_KEY}`,
    SLID: `slid${EVENT_KEY}`,
    KEYDOWN: `keydown${EVENT_KEY}`,
    MOUSEENTER: `mouseenter${EVENT_KEY}`,
    MOUSELEAVE: `mouseleave${EVENT_KEY}`,
    TOUCHSTART: `touchstart${EVENT_KEY}`,
    TOUCHMOVE: `touchmove${EVENT_KEY}`,
    TOUCHEND: `touchend${EVENT_KEY}`,
    POINTERDOWN: `pointerdown${EVENT_KEY}`,
    POINTERUP: `pointerup${EVENT_KEY}`,
    DRAG_START: `dragstart${EVENT_KEY}`,
    LOAD_DATA_API: `load${EVENT_KEY}${DATA_API_KEY}`,
    CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`
}

const ClassName = {
    CAROUSEL: 'carousel',
    ACTIVE: 'active',
    SLIDE: 'slide',
    RIGHT: 'carousel-item-right',
    LEFT: 'carousel-item-left',
    NEXT: 'carousel-item-next',
    PREV: 'carousel-item-prev',
    ITEM: 'carousel-item',
    POINTER_EVENT: 'pointer-event'
}

const Selector = {
    ACTIVE: '.active',
    ACTIVE_ITEM: '.active.carousel-item',
    ITEM: '.carousel-item',
    ITEM_IMG: '.carousel-item img',
    NEXT_PREV: '.carousel-item-next, .carousel-item-prev',
    INDICATORS: '.carousel-indicators',
    DATA_SLIDE: '[data-slide], [data-slide-to]',
    DATA_RIDE: '[data-ride="carousel"]'
}

const PointerType = {
    TOUCH: 'touch',
    PEN: 'pen'
}

/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */
class Carousel {
    constructor(element, config) {
        this._items = null
        this._interval = null
        this._activeElement = null
        this._isPaused = false
        this._isSliding = false
        this.touchTimeout = null
        this.touchStartX = 0
        this.touchDeltaX = 0

        this._config = this._getConfig(config)
        this._element = element
        this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)
        this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
        this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)

        this._addEventListeners()
    }

    // Getters

    static get VERSION() {
        return VERSION
    }

    static get Default() {
        return Default
    }

    // Public

    next() {
        if (!this._isSliding) {
            this._slide(Direction.NEXT)
        }
    }

    nextWhenVisible() {
        // Don't call next when the page isn't visible
        // or the carousel or its parent isn't visible
        if (!document.hidden &&
            ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {
            this.next()
        }
    }

    prev() {
        if (!this._isSliding) {
            this._slide(Direction.PREV)
        }
    }

    pause(event) {
        if (!event) {
            this._isPaused = true
        }

        if (this._element.querySelector(Selector.NEXT_PREV)) {
            Util.triggerTransitionEnd(this._element)
            this.cycle(true)
        }

        clearInterval(this._interval)
        this._interval = null
    }

    cycle(event) {
        if (!event) {
            this._isPaused = false
        }

        if (this._interval) {
            clearInterval(this._interval)
            this._interval = null
        }

        if (this._config.interval && !this._isPaused) {
            this._interval = setInterval(
                (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
                this._config.interval
            )
        }
    }

    to(index) {
        this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)

        const activeIndex = this._getItemIndex(this._activeElement)

        if (index > this._items.length - 1 || index < 0) {
            return
        }

        if (this._isSliding) {
            $(this._element).one(Event.SLID, () => this.to(index))
            return
        }

        if (activeIndex === index) {
            this.pause()
            this.cycle()
            return
        }

        const direction = index > activeIndex ?
            Direction.NEXT :
            Direction.PREV

        this._slide(direction, this._items[index])
    }

    dispose() {
        $(this._element).off(EVENT_KEY)
        $.removeData(this._element, DATA_KEY)

        this._items = null
        this._config = null
        this._element = null
        this._interval = null
        this._isPaused = null
        this._isSliding = null
        this._activeElement = null
        this._indicatorsElement = null
    }

    // Private

    _getConfig(config) {
        config = {
            ...Default,
            ...config
        }
        Util.typeCheckConfig(NAME, config, DefaultType)
        return config
    }

    _handleSwipe() {
        const absDeltax = Math.abs(this.touchDeltaX)

        if (absDeltax <= SWIPE_THRESHOLD) {
            return
        }

        const direction = absDeltax / this.touchDeltaX

        // swipe left
        if (direction > 0) {
            this.prev()
        }

        // swipe right
        if (direction < 0) {
            this.next()
        }
    }

    _addEventListeners() {
        if (this._config.keyboard) {
            $(this._element)
                .on(Event.KEYDOWN, (event) => this._keydown(event))
        }

        if (this._config.pause === 'hover') {
            $(this._element)
                .on(Event.MOUSEENTER, (event) => this.pause(event))
                .on(Event.MOUSELEAVE, (event) => this.cycle(event))
        }

        if (this._config.touch) {
            this._addTouchEventListeners()
        }
    }

    _addTouchEventListeners() {
        if (!this._touchSupported) {
            return
        }

        const start = (event) => {
            if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
                this.touchStartX = event.originalEvent.clientX
            } else if (!this._pointerEvent) {
                this.touchStartX = event.originalEvent.touches[0].clientX
            }
        }

        const move = (event) => {
            // ensure swiping with one touch and not pinching
            if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {
                this.touchDeltaX = 0
            } else {
                this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX
            }
        }

        const end = (event) => {
            if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
                this.touchDeltaX = event.originalEvent.clientX - this.touchStartX
            }

            this._handleSwipe()
            if (this._config.pause === 'hover') {
                // If it's a touch-enabled device, mouseenter/leave are fired as
                // part of the mouse compatibility events on first tap - the carousel
                // would stop cycling until user tapped out of it;
                // here, we listen for touchend, explicitly pause the carousel
                // (as if it's the second time we tap on it, mouseenter compat event
                // is NOT fired) and after a timeout (to allow for mouse compatibility
                // events to fire) we explicitly restart cycling

                this.pause()
                if (this.touchTimeout) {
                    clearTimeout(this.touchTimeout)
                }
                this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
            }
        }

        $(this._element.querySelectorAll(Selector.ITEM_IMG)).on(Event.DRAG_START, (e) => e.preventDefault())
        if (this._pointerEvent) {
            $(this._element).on(Event.POINTERDOWN, (event) => start(event))
            $(this._element).on(Event.POINTERUP, (event) => end(event))

            this._element.classList.add(ClassName.POINTER_EVENT)
        } else {
            $(this._element).on(Event.TOUCHSTART, (event) => start(event))
            $(this._element).on(Event.TOUCHMOVE, (event) => move(event))
            $(this._element).on(Event.TOUCHEND, (event) => end(event))
        }
    }

    _keydown(event) {
        if (/input|textarea/i.test(event.target.tagName)) {
            return
        }

        switch (event.which) {
            case ARROW_LEFT_KEYCODE:
                event.preventDefault()
                this.prev()
                break
            case ARROW_RIGHT_KEYCODE:
                event.preventDefault()
                this.next()
                break
            default:
        }
    }

    _getItemIndex(element) {
        this._items = element && element.parentNode ?
            [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM)) :
            []
        return this._items.indexOf(element)
    }

    _getItemByDirection(direction, activeElement) {
        const isNextDirection = direction === Direction.NEXT
        const isPrevDirection = direction === Direction.PREV
        const activeIndex = this._getItemIndex(activeElement)
        const lastItemIndex = this._items.length - 1
        const isGoingToWrap = isPrevDirection && activeIndex === 0 ||
            isNextDirection && activeIndex === lastItemIndex

        if (isGoingToWrap && !this._config.wrap) {
            return activeElement
        }

        const delta = direction === Direction.PREV ? -1 : 1
        const itemIndex = (activeIndex + delta) % this._items.length

        return itemIndex === -1 ?
            this._items[this._items.length - 1] : this._items[itemIndex]
    }

    _triggerSlideEvent(relatedTarget, eventDirectionName) {
        const targetIndex = this._getItemIndex(relatedTarget)
        const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))
        const slideEvent = $.Event(Event.SLIDE, {
            relatedTarget,
            direction: eventDirectionName,
            from: fromIndex,
            to: targetIndex
        })

        $(this._element).trigger(slideEvent)

        return slideEvent
    }

    _setActiveIndicatorElement(element) {
        if (this._indicatorsElement) {
            const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))
            $(indicators)
                .removeClass(ClassName.ACTIVE)

            const nextIndicator = this._indicatorsElement.children[
                this._getItemIndex(element)
            ]

            if (nextIndicator) {
                $(nextIndicator).addClass(ClassName.ACTIVE)
            }
        }
    }

    _slide(direction, element) {
        const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)
        const activeElementIndex = this._getItemIndex(activeElement)
        const nextElement = element || activeElement &&
            this._getItemByDirection(direction, activeElement)
        const nextElementIndex = this._getItemIndex(nextElement)
        const isCycling = Boolean(this._interval)

        let directionalClassName
        let orderClassName
        let eventDirectionName

        if (direction === Direction.NEXT) {
            directionalClassName = ClassName.LEFT
            orderClassName = ClassName.NEXT
            eventDirectionName = Direction.LEFT
        } else {
            directionalClassName = ClassName.RIGHT
            orderClassName = ClassName.PREV
            eventDirectionName = Direction.RIGHT
        }

        if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {
            this._isSliding = false
            return
        }

        const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
        if (slideEvent.isDefaultPrevented()) {
            return
        }

        if (!activeElement || !nextElement) {
            // Some weirdness is happening, so we bail
            return
        }

        this._isSliding = true

        if (isCycling) {
            this.pause()
        }

        this._setActiveIndicatorElement(nextElement)

        const slidEvent = $.Event(Event.SLID, {
            relatedTarget: nextElement,
            direction: eventDirectionName,
            from: activeElementIndex,
            to: nextElementIndex
        })

        if ($(this._element).hasClass(ClassName.SLIDE)) {
            $(nextElement).addClass(orderClassName)

            Util.reflow(nextElement)

            $(activeElement).addClass(directionalClassName)
            $(nextElement).addClass(directionalClassName)

            const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)
            if (nextElementInterval) {
                this._config.defaultInterval = this._config.defaultInterval || this._config.interval
                this._config.interval = nextElementInterval
            } else {
                this._config.interval = this._config.defaultInterval || this._config.interval
            }

            const transitionDuration = Util.getTransitionDurationFromElement(activeElement)

            $(activeElement)
                .one(Util.TRANSITION_END, () => {
                    $(nextElement)
                        .removeClass(`${directionalClassName} ${orderClassName}`)
                        .addClass(ClassName.ACTIVE)

                    $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)

                    this._isSliding = false

                    setTimeout(() => $(this._element).trigger(slidEvent), 0)
                })
                .emulateTransitionEnd(transitionDuration)
        } else {
            $(activeElement).removeClass(ClassName.ACTIVE)
            $(nextElement).addClass(ClassName.ACTIVE)

            this._isSliding = false
            $(this._element).trigger(slidEvent)
        }

        if (isCycling) {
            this.cycle()
        }
    }

    // Static

    static _jQueryInterface(config) {
        return this.each(function() {
            let data = $(this).data(DATA_KEY)
            let _config = {
                ...Default,
                ...$(this).data()
            }

            if (typeof config === 'object') {
                _config = {
                    ..._config,
                    ...config
                }
            }

            const action = typeof config === 'string' ? config : _config.slide

            if (!data) {
                data = new Carousel(this, _config)
                $(this).data(DATA_KEY, data)
            }

            if (typeof config === 'number') {
                data.to(config)
            } else if (typeof action === 'string') {
                if (typeof data[action] === 'undefined') {
                    throw new TypeError(`No method named "${action}"`)
                }
                data[action]()
            } else if (_config.interval && _config.ride) {
                data.pause()
                data.cycle()
            }
        })
    }

    static _dataApiClickHandler(event) {
        const selector = Util.getSelectorFromElement(this)

        if (!selector) {
            return
        }

        const target = $(selector)[0]

        if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {
            return
        }

        const config = {
            ...$(target).data(),
            ...$(this).data()
        }
        const slideIndex = this.getAttribute('data-slide-to')

        if (slideIndex) {
            config.interval = false
        }

        Carousel._jQueryInterface.call($(target), config)

        if (slideIndex) {
            $(target).data(DATA_KEY).to(slideIndex)
        }

        event.preventDefault()
    }
}

/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */

$(document)
    .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)

$(window).on(Event.LOAD_DATA_API, () => {
    const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))
    for (let i = 0, len = carousels.length; i < len; i++) {
        const $carousel = $(carousels[i])
        Carousel._jQueryInterface.call($carousel, $carousel.data())
    }
})

/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
 */

$.fn[NAME] = Carousel._jQueryInterface
$.fn[NAME].Constructor = Carousel
$.fn[NAME].noConflict = () => {
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Carousel._jQueryInterface
}

export default Carousel